import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt

# 读取数据集, header来指定标题的行, 默认为0, 如果数据集没有标题,使用None

data = pd.read_csv('data/iris.csv', header=0)

# 显示前 n 行数据 默认5
# data.head(n)
# 显示末尾 n 行, 默认5
# data.tail(n)
# 来随机抽取 n 条数据, 默认1
# data.sample(n)

# 将字符文本映射成数值类型, 便于对数据进行处理
data['class'] = data['class'].map({'Iris-setosa': 0, 'Iris-virginica': 1, 'Iris-versicolor': 2})

# 删除不需要的列, 便于数据的处理, inplace来进行就地删除
# data.drop('id', axis=1, inplace=True)

# data.duplicated() 查看是否有重复, 调用any方法可以直接返回结果中是否有True
# data.duplicated().any()

# 去重
data.drop_duplicates(inplace=True)

# 查看各个类别的花有多少条记录
data['class'].value_counts()


class KNN:
    """
    K近邻算法 ( 实现分类)
    """
    def __init__(self, k):
        """
        邻居的个数 k: int
        """
        self.k = k

    def fit(self, X, y):
        """
        用来训练的方法 (惰性学习)
        :param X: 类似数组类型, 形状为 [样本数量, 特征数量] 带训练的样本特征
        :param y: 类数组类型, 形状为 [样本数量] 每个样本的目标值(标签)
        x : ndarray
        y : ndarray
        """
        # 将X转化成 ndarray 数组类型
        self.X = np.array(X)
        self.y = np.array(y)

    def predict(self, X):
        """
        根据参数传递的样本, 对样本数据进行预测
        :param X: 类似数组类型, 形状为 [样本数量, 特征数量] 带训练的样本特征
        :return: 数组类型
        """
        X = np.array(X)
        result = []
        # 对 ndarray 数组进行遍历, 每次数组中的一行
        for x in X:
            # 对于测试集中的每一个样本， 依次
            dis = np.sqrt(np.sum((x - self.X) ** 2, axis=1))

            # 返回数组排序后， 每个元素在原数组中的索引
            index = dis.argsort()

            # 进行截断， 取前k个元素【取距离最近的k个元素的索引】
            index = index[:self.k]

            # 返回数组中每个元素出现的次数， 元素必须是非负
            count = np.bincount(self.y[index])

            # 返回ndarray数组中， 值最大的元素对应的索引， 该索引就是我们判断的类别, 最大元素就是出现最多的元素
            result.append(count.argmax())

        return np.asarray(result)

    def predict2(self, X):
        """
        考虑权重, 使用距离的倒数作为权重
        根据参数传递的样本, 对样本数据进行预测
        :param X: 类似数组类型, 形状为 [样本数量, 特征数量] 带训练的样本特征
        :return: 数组类型
        """
        X = np.array(X)
        result = []
        # 对 ndarray 数组进行遍历, 每次数组中的一行
        for x in X:
            # 对于测试集中的每一个样本， 依次
            dis = np.sqrt(np.sum((x - self.X) ** 2, axis=1))

            # 返回数组排序后， 每个元素在原数组中的索引
            index = dis.argsort()

            # 进行截断， 取前k个元素【取距离最近的k个元素的索引】
            index = index[:self.k]

            # 使用weight
            count = np.bincount(self.y[index], weights=1/dis[index])

            # 返回ndarray数组中， 值最大的元素对应的索引， 该索引就是我们判断的类别, 最大元素就是出现最多的元素
            result.append(count.argmax())

        return np.asarray(result)


# 提取每个类别的花的数据
t0 = data[data['class'] == 0]
t1 = data[data['class'] == 1]
t2 = data[data['class'] == 2]

# 改变数据的顺序, 使数据有随机性. random_state 使打乱顺序相同, 指定多少无所谓, 主要使进行数据打乱的重现
t0 = t0.sample(len(t0), random_state=1)  # len(t0) 便于动态获取数据
t1 = t1.sample(len(t1), random_state=1)
t2 = t2.sample(len(t2), random_state=1)

# 合并划分后的数据集, 用来构建数据集和测试集
train_X = pd.concat([t0.iloc[:40, :-1],
                    t1.iloc[:40, :-1],
                    t2.iloc[:40, :-1]], axis=0)  # 划分40行数据, 不包含最后一列的数据集

train_y = pd.concat([t0.iloc[:40, -1],
                    t1.iloc[:40, -1],
                    t2.iloc[:40, -1]], axis=0)

test_X = pd.concat([t0.iloc[40:, :-1],
                    t1.iloc[40:, :-1],
                    t2.iloc[40:, :-1]], axis=0)
test_y = pd.concat([t0.iloc[40:, -1],
                    t1.iloc[40:, -1],
                    t2.iloc[40:, -1]], axis=0)
# 创建 KNN对象, 进行训练和测试

knn = KNN(k=3)

# 进行训练
knn.fit(X=train_X, y=train_y)

# 进行测试,获得测试结果
result = knn.predict(X=test_X)
# print(test_y)
# print(result)

# 查看预测结果的正确性
print(np.sum(result == test_y))


# KNN 可视化
# 设置画布的大小
plt.figure(figsize=(10, 10))
# 设置字体
mpl.rcParams['font.family'] = 'SimHei'
# 设置能正常显示负号
mpl.rcParams['axes.unicode_minus'] = False

# 绘制训练集数据散点图
plt.scatter(x=t0['sepallength'][:40], y=t0['petallength'][:40], color='r', label='Iris-setosa')
plt.scatter(x=t1['sepallength'][:40], y=t1['petallength'][:40], color='g', label='Iris-virginica')
plt.scatter(x=t2['sepallength'][:40], y=t2['petallength'][:40], color='b', label='Iris-versicolor')

# 绘制测试集数据
right = test_X[result == test_y]  # 预测结果为正确的数据
wrong = test_X[result != test_y]  # 预测结果为错误的数据

plt.scatter(x=right['sepallength'], y=right['petallength'], color='c', marker='x', label='right')
plt.scatter(x=wrong['sepallength'], y=wrong['petallength'], color='m', marker='>', label='wrong')

plt.xlabel('花萼长度')
plt.ylabel('花瓣长度')
plt.title('KNN 分类结果')
plt.legend(loc='best')
plt.show()

